/*
 * GChampionsLeagueImpl.java
 * Created on 2 nov. 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.impl.contest;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.club.GClubAbstractIntCupHistoryRecord;
import com.hockeo.vhbase.data.dao.club.GClubCLHistoryRecord;
import com.hockeo.vhbase.data.dao.club.GClubWLHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContest;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.contest.GContestRound;
import com.hockeo.vhbase.data.dao.contest.GKO3ContestElement;
import com.hockeo.vhbase.data.dao.contest.GRoundRobinContestElement;
import com.hockeo.vhbase.data.dao.contest.GWorldLeague;
import com.hockeo.vhbase.data.dao.match.GKOMatch;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.data.dao.season.GSeason;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.data.helper.GContestRoundType;
import com.hockeo.vhbase.data.helper.GContestUtils;
import com.hockeo.vhbase.data.helper.GIntCupParticipant;
import com.hockeo.vhbase.error.GDataException;
import com.hockeo.vhbase.error.GException;

/**
 * Standard implementation for the international contest "Champions League".
 *
 * @version $Id$
 * @author jjanke
 */
@Transactional( propagation = Propagation.MANDATORY, rollbackFor = { GException.class } )
public class GChampionsLeagueImpl extends GIntCupImpl
{
  private static Log d_logger = LogFactory.getLog( GChampionsLeagueImpl.class );

  /**
   * Creates the contest structure, determines which teams are qualified for the contest
   * and if more than 18 teams are qualified determines which teams have to play the
   * qualification.
   */
  public void createStructure( GSeason season, GContest contest ) throws GDataException
  {
    if ( contest.getNumTeams() != 18 )
      throw new IllegalStateException(
          "The Champions League Implementation currently expects not more than 18 main round participants." );

    List<GIntCupParticipant> list = d_dao.findClubsForChampionsLeague( season );
    GContestRound roundQR = null, roundMR = null;
    int nNumRound = 1;
    int nNumDirectlyQualified = list.size();

    if ( list.size() > contest.getNumTeams() * 2 )
      throw new IllegalStateException( "A maximum number of " + ( contest.getNumTeams() * 2 )
          + " participants is supported (" + list.size() + " participants determined)." );

    // Qualification (standard KO round)
    //
    if ( nNumDirectlyQualified > contest.getNumTeams() )
    {
      roundQR = GContestUtils.createKORound( contest, nNumRound++, GContestRoundType.QUALIFICATION_1, 19, list.size()
          - contest.getNumTeams(), false );

      nNumDirectlyQualified = contest.getNumTeams() - ( list.size() - contest.getNumTeams() );
    }
    else if ( list.size() < contest.getNumTeams() )
    {
      d_logger.warn( "No Champions League will be configured because not enough participants have been determined "
          + "(" + contest.getNumTeams() + " required, " + list.size() + " determined)." );

      return;
    }

    // Main round (3 groups with 6 teams each)
    //
    roundMR = GContestUtils.createRoundRobinRound( contest, nNumRound++, GContestRoundType.ROUND_MAIN, 10, 3, 6, 2, 1,
        2, 0, false );

    // Semi final qualification (qualified are 2nd and 3rd ranked teams from the main
    // round)
    //
    GContestUtils.createKORound( contest, nNumRound++, GContestRoundType.QUARTER_FINAL, 7, 3, true );

    // Semi final (winners of main round and QF winners are qualified)
    //
    GContestUtils.createKORound( contest, nNumRound++, GContestRoundType.SEMI_FINAL, 4, 3, false );

    // Final (single round three team tournament)
    //
    GContestRound roundFinal = GContestUtils.createRoundRobinRound( contest, nNumRound++, GContestRoundType.FINAL, 2,
        1, 3, 1, 1, 0, 0, false );
    roundFinal.setLastRound( true );

    // Persist the created rounds
    //
    int nNumLeftKOMatches = 0;
    int nNumLeftRRMatches = 0;
    for ( int i = contest.getContestRounds().size() - 1; i >= 0; i-- )
    {
      GContestRound round = contest.getContestRounds().get( i );
      GContestElement elementFirst = round.getContestElements().get( 0 );

      // Set the matches that remain to be played after this round
      //
      round.setRemainingKOMatchesSameGA( nNumLeftKOMatches );
      round.setRemainingRRMatchesSameGA( nNumLeftRRMatches );

      // Update the match variables after they have been set to the current round so they
      // are up-to-date for the next round
      //
      if ( elementFirst.instanceOf( GRoundRobinContestElement.class ) )
        nNumLeftRRMatches += elementFirst.getNumMatchesPerClub();
      else if ( elementFirst.instanceOf( GKO3ContestElement.class ) )
        nNumLeftKOMatches += 2;
      else
        throw new IllegalStateException( "Unsupported contest element type: "
            + elementFirst.getGEntityClass().getName() );

      d_dao.makePersistent( round );
    }

    // Assign clubs to main or qualification round
    //
    int i = 0;
    while ( i < nNumDirectlyQualified )
    {
      GIntCupParticipant part = list.get( i++ );
      GClub club = d_dao.findEntity( GClub.class, part.getClubId() );
      GClubCLHistoryRecord chr = new GClubCLHistoryRecord( season, club, roundMR, part.getSeeding() );

      d_dao.makePersistent( chr );
    }

    while ( i < list.size() )
    {
      GIntCupParticipant part = list.get( i++ );
      GClub club = d_dao.findEntity( GClub.class, part.getClubId() );
      GClubCLHistoryRecord chr = new GClubCLHistoryRecord( season, club, roundQR, part.getSeeding() );

      d_dao.makePersistent( chr );
    }
  }

  public void assignMatchdaysToMovedays( GContest contest ) throws GDataException
  {
    final boolean fQualification = contest.getContestRounds().size() == 5;
    final int nNumRequiredMovedays = fQualification ? 9 : 8;
    List<GMoveday> listAvailableMovedays = d_dao.findReservedMovedays( contest );

    // Check if we have enough movedays and remove superflous rounds at the beginning of
    // the whole contest.
    //
    if ( listAvailableMovedays.size() < nNumRequiredMovedays )
      throw new GDataException( "There are not enough reserved movedays for the CL available.\nThe contest "
          + contest.getId() + " requires " + nNumRequiredMovedays + " movedays, but only "
          + listAvailableMovedays.size() + " are available." );
    else if ( listAvailableMovedays.size() > nNumRequiredMovedays )
    {
      int nToBeRemoved = listAvailableMovedays.size() - nNumRequiredMovedays;

      for ( int i = 0; i < nToBeRemoved; i++ )
        listAvailableMovedays.remove( 0 );
    }

    // Do the actual attachment of matchdays
    //
    int iStartIndex = 0;
    for ( GContestRound round : contest )
    {
      switch ( round.getRoundType() )
      {
        case QUALIFICATION_1:
        {
          for ( GContestElement element : round )
            for ( GMatchday matchday : d_dao.findMatchdays( element ) )
              listAvailableMovedays.get( iStartIndex ).addMatchday( matchday );

          iStartIndex++;
          break;
        }
        case ROUND_MAIN:
        {
          for ( GContestElement element : round )
            element.attachMatchdaysToMovedays( listAvailableMovedays.subList( iStartIndex, iStartIndex + 5 ),
                d_dao.findMatchdays( element ), true );

          iStartIndex += 5;
          break;
        }
        case QUARTER_FINAL:
        case SEMI_FINAL:
        {
          for ( GContestElement element : round )
            for ( GMatchday matchday : d_dao.findMatchdays( element ) )
              listAvailableMovedays.get( iStartIndex ).addMatchday( matchday );

          iStartIndex++;
          break;
        }
        case FINAL:
        {
          for ( GContestElement element : round )
            element.attachMatchdaysToMovedays( listAvailableMovedays.subList( iStartIndex, iStartIndex + 1 ),
                d_dao.findMatchdays( element ), true );

          iStartIndex++;
          break;
        }
        default:
          throw new IllegalStateException( "Unexpected CL round type: " + round.getRoundType().name() );
      }
    }
  }

  /**
   * Overridden to create special match pairings for the quarter and semi finals depending
   * on the main round results.
   */
  @Override
  protected void createKO3Matches( GSeason season, GKO3ContestElement element ) throws GDataException
  {
    final GContestRound round = element.getContestRound();

    // If it is not the semi- or quarter-final, fall back to the standard match creation
    // algorithm
    //
    if ( round.getRoundType() != GContestRoundType.QUARTER_FINAL
        && round.getRoundType() != GContestRoundType.SEMI_FINAL )
    {
      super.createKO3Matches( season, element );
      return;
    }

    // Only create matches, if none has been created so far
    //
    if ( d_dao.findNumMatches( element ) > 0 )
      return;

    if ( round.getContestElements().size() != 1 )
      throw new IllegalArgumentException( "The contest element " + element.getId() + " belongs to a KO round "
          + "that does not have exactly one contest element." );

    final List<GGoalsAccount> listGAs = d_dao.findGoalsAccounts( element );
    final Map<String, GClub> mapClubs = new HashMap<String, GClub>();
    final List<GMatchday> listMatchdays = d_dao.findMatchdays( element );

    if ( listMatchdays == null || listMatchdays.isEmpty() )
      throw new IllegalStateException( "No matchday is available for contest element " + element.getId() );

    // add all clubs to the map using the combination of the last pool and the last pool
    // rank as keys (e.g. A1, B2, B3, C1 etc.) if the club has last played in a KO round
    // and the name of the KO match if the last round was a KO round (e.g. QF2, SF1 etc.).
    //
    for ( GGoalsAccount ga : listGAs )
    {
      GClubCLHistoryRecord chr = d_dao.findClubHistoryRecord( GClubCLHistoryRecord.class, round.getContest(), season,
          ga.getClub() );

      switch ( chr.getLastRoundFormat() )
      {
        case KNOCK_OUT:
          mapClubs.put( chr.getLastKOMatchName(), ga.getClub() );
          break;

        case ROUND_ROBIN:
          mapClubs.put( chr.getLastPool() + chr.getLastPoolRank(), ga.getClub() );
          break;

        default:
          throw new IllegalStateException( "Unsupported contest round format " + round.getFormat().name() );
      }
    }

    if ( round.getRoundType() == GContestRoundType.QUARTER_FINAL )
    {
      assert mapClubs.size() == 6 : "For the CL quarter final, exactly 6 clubs must be qualified, but there are currently "
          + mapClubs.size() + ".";

      d_dao.makePersistent( new GKOMatch( "QF1", element, mapClubs.get( "A2" ), mapClubs.get( "B3" ), listMatchdays ) );
      d_dao.makePersistent( new GKOMatch( "QF2", element, mapClubs.get( "B2" ), mapClubs.get( "C3" ), listMatchdays ) );
      d_dao.makePersistent( new GKOMatch( "QF3", element, mapClubs.get( "C2" ), mapClubs.get( "A3" ), listMatchdays ) );
    }
    else if ( round.getRoundType() == GContestRoundType.SEMI_FINAL )
    {
      assert mapClubs.size() == 6 : "For the CL semi final, exactly 6 clubs must be qualified, but there are currently "
          + mapClubs.size() + ".";

      d_dao.makePersistent( new GKOMatch( element, mapClubs.get( "A1" ), mapClubs.get( "QF2" ), listMatchdays ) );
      d_dao.makePersistent( new GKOMatch( element, mapClubs.get( "B1" ), mapClubs.get( "QF3" ), listMatchdays ) );
      d_dao.makePersistent( new GKOMatch( element, mapClubs.get( "C1" ), mapClubs.get( "QF1" ), listMatchdays ) );
    }
  }

  /**
   * Attaches CL qualification losers to first round of the World League.
   */
  @Override
  protected void handleKORoundLosers( final GSeason season, final GKO3ContestElement element,
      final ArrayList<GClubAbstractIntCupHistoryRecord> listLosers )
  {
    if ( element.getContestRound().getRoundType() != GContestRoundType.QUALIFICATION_1 )
      return;

    GWorldLeague wl = d_dao.findEntity( GWorldLeague.class, GRulesConstants.SPECIAL_CONTEST_ID_WORLD_LEAGUE );

    // Search first WL round
    //
    GContestRound roundWLFirst = null;
    for ( GContestRound round : wl.getContestRounds() )
      if ( round.getRoundType() == GContestRoundType.ROUND_MAIN )
        roundWLFirst = round;

    // Attach CL quali losers to first round of WL
    //
    for ( GClubAbstractIntCupHistoryRecord chrLoser : listLosers )
    {
      GClubWLHistoryRecord chr = new GClubWLHistoryRecord( season, chrLoser.getClub(), roundWLFirst, 0 );

      d_dao.makePersistent( chr );
    }
  }

  @Override
  protected void resetHandleKORoundLosers( final GSeason season, final GKO3ContestElement element,
      final ArrayList<GClubAbstractIntCupHistoryRecord> listLosers )
  {
    if ( element.getContestRound().getRoundType() != GContestRoundType.QUALIFICATION_1 )
      return;

    GWorldLeague wl = d_dao.findEntity( GWorldLeague.class, GRulesConstants.SPECIAL_CONTEST_ID_WORLD_LEAGUE );

    // Search corresponding WL contest history records and delete them
    //
    for ( GClubAbstractIntCupHistoryRecord chrLoser : listLosers )
    {
      GClubWLHistoryRecord chr = d_dao.findClubHistoryRecord( GClubWLHistoryRecord.class, wl, season,
          chrLoser.getClub() );

      if ( chr != null )
        d_dao.delete( chr );
    }
  }
}
